//--------------------------------------------------------------------------
// $Id: HTMLBasedDiscoverer.java 4 2006-08-20 18:34:48Z zendragon $
//--------------------------------------------------------------------------
// Copyright 2006 Social Brain Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//--------------------------------------------------------------------------

package org.socialbrain.openid.consumer;

import org.socialbrain.openid.Constants;
import org.socialbrain.openid.OpenIdException;
import org.socialbrain.openid.Utils;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.htmlparser.Node;
import org.htmlparser.Tag;
import org.htmlparser.lexer.Lexer;
import org.htmlparser.nodes.TagNode;
import org.htmlparser.util.ParserException;

import java.io.IOException;

/**
 * <a href="http://www.openid.net/specs/openid-authentication-2_0-08.html#anchor11">HTML-Based Discovery</a>
 * implementation.
 *
 * It uses <a href="http://jakarta.apache.org/commons/httpclient/">Jakarta's HttpClient</a> to fetch html page
 * and <a href="http://htmlparser.sourceforge.net/">HTMLParser</a> to parse the fetched html.
 *
 * @author <a href="http://erning.net/">Zhang Erning</a>
 * @version $Id: HTMLBasedDiscoverer.java 4 2006-08-20 18:34:48Z zendragon $
 */
public class HTMLBasedDiscoverer implements Discoverer
{
    /**
     * Obtains 'openid.server' and/or 'openid.delegate' by fetch and parse the HTML
     * located at the identifier URL. The claimed identifier will be set as the final
     * destination URL.
     *
     * @param identifier The identifier is an abstract URL.
     * @return Discovered information or null on fail.
     * @throws OpenIdException if unable to fetch or parse the HTML
     */
    public Discovered discover(String identifier)
    {
        if (!Utils.isHTTP(identifier))
        {
            throw new IllegalArgumentException("Accept HTTP URL identifier only.");
        }

        Discovered result = new Discovered();
        try
        {
            String html = fetchHTML(identifier, result);
            parseHTML(html, result);
        }
        catch (IOException e)
        {
            throw new OpenIdException(String.format("Unable to fetch html from %s", identifier), e);
        }
        catch (ParserException e)
        {
            throw new OpenIdException("Unable to parse fetched html", e);
        }
        return result;
    }

    private String fetchHTML(String url, Discovered result) throws IOException
    {
        GetMethod method = new GetMethod(url);
        try
        {
            method.setFollowRedirects(true);
            if (_httpClient.executeMethod(method) != HttpStatus.SC_OK)
            {
                throw new OpenIdException(String.format("Unexcept http status: %03d - %s",
                        method.getStatusCode(), method.getStatusText()));
            }
            result.setClaimedIdentifier(method.getURI().toString());
            if (_logger.isDebugEnabled())
            {
                _logger.debug("ClaimedIdentifier set as " + result.getClaimedIdentifier());
            }
            return Utils.readStringFromStream(
                    method.getResponseBodyAsStream(),
                    method.getResponseContentLength(),
                    method.getResponseCharSet());
        }
        finally
        {
            method.releaseConnection();
        }
    }

    private void parseHTML(String html, Discovered result) throws ParserException
    {
        Lexer lexer = new Lexer(html);
        Node node;
        int insideHead = 0;
        while (insideHead < 2 && (result.getEndpoint() == null || result.getDelegateIdentifier() == null))
        {
            node = lexer.nextNode();
            if (node == null) break;
            if (!(node instanceof TagNode)) continue;
            Tag tag = (Tag)node;

            if ("HEAD".equalsIgnoreCase(tag.getTagName()))
            {
                insideHead++;
            }
            else if (insideHead == 1 && "LINK".equalsIgnoreCase(tag.getTagName()))
            {   // inside head section, looking up for <link ref... />
                String rel = tag.getAttribute("REL");
                if (result.getEndpoint() == null && Constants.LINK_REL_OPENID_SERVER.equals(rel))
                {
                    result.setEndpoint(tag.getAttribute("HREF"));
                    if (_logger.isDebugEnabled())
                    {
                        _logger.debug("Endpoint set as " + result.getEndpoint());
                    }
                }
                else if (result.getDelegateIdentifier() == null && Constants.LINK_REL_OPENID_DELEGATE.equals(rel))
                {
                    result.setDelegateIdentifier(tag.getAttribute("HREF"));
                    if (_logger.isDebugEnabled())
                    {
                        _logger.debug("DelegateIdentifier set as " + result.getDelegateIdentifier());
                    }
                }
            }
        }
    }

    //

    public void setHttpClient(HttpClient value)
    {
        _httpClient = value;
    }

    public HttpClient getHttpClient()
    {
        return _httpClient;
    }

    private HttpClient _httpClient;

    private transient static final Log _logger = LogFactory.getLog(HTMLBasedDiscoverer.class);
}
